/**
 * Copyright 1998-2006, CHISEL Group, University of Victoria, Victoria, BC, Canada.
 * All rights reserved.
 */
package ca.uvic.cs.cogz.mapping.core;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javax.swing.tree.TreePath;

import ca.uvic.cs.cogz.mapping.core.MappingTypeManager.MappingType;
import ca.uvic.cs.cogz.mapping.filter.CandidationOperationFilter;
import ca.uvic.cs.cogz.mapping.filter.TreeFilterManager;
import ca.uvic.cs.cogz.mapping.ontology.DataLoaderFacade;
import ca.uvic.cs.cogz.util.FrameUtil;
import edu.stanford.smi.protege.model.Cls;
import edu.stanford.smi.protege.model.Frame;
import edu.stanford.smi.protege.util.CollectionUtilities;
import edu.stanford.smi.protege.util.LazyTreeNode;
import edu.stanford.smi.protegex.prompt.actionLists.SuggestionsAndConflicts;
import edu.stanford.smi.protegex.prompt.explanation.Explanation;
import edu.stanford.smi.protegex.prompt.explanation.IdenticalNames;
import edu.stanford.smi.protegex.prompt.explanation.RejectedMapping;
import edu.stanford.smi.protegex.prompt.operation.MergeFramesOperation;
import edu.stanford.smi.protegex.prompt.operation.Operation;
import edu.stanford.smi.protegex.prompt.plugin.PluginManager;
import edu.stanford.smi.protegex.prompt.plugin.model.MappingStoragePlugin;

/**
 * Class for storing and accessing all mappings.
 * 
 * @author Sean Falconer
 * @date 10-Jan-07
 */
public class MappingCollection {
	/** collection of MapElement objects, one for each mapping */
	private ArrayList<AbstractMapElement> mappedElements;

	/** collection of paths from the root element to the actually mapped element */
	private TreeSet<Collection<Frame>> sourcePaths;
	private TreeSet<Collection<Frame>> targetPaths;

	/** collection of frames that have been mapped from source and target */
	private Map<Frame, MappingType> sourceMappedFrames;
	private Map<Frame, MappingType> targetMappedFrames;

	/** mapping of source and target frames to mapping elements */
	private Map<Frame, List<AbstractMapElement>> sourceFrameToMapElements;
	private Map<Frame, List<AbstractMapElement>> targetFrameToMapElements;

	private Map<Frame, Integer> frameToNumOfMappings;

	/** manager for the available mapping types */
	private MappingTypeManager mappingTypeManager;

	private CandidationOperationFilter operationFilter;

	private int largestNumOfMappings;

	/** collection of frames marked interesting */
	private HashMap<Frame, Boolean> interestingFrames;

	public MappingCollection() {
		this.mappedElements = new ArrayList<AbstractMapElement>();
		this.sourcePaths = new TreeSet<Collection<Frame>>(new FramePathComparator());
		this.targetPaths = new TreeSet<Collection<Frame>>(new FramePathComparator());
		this.sourceMappedFrames = new HashMap<Frame, MappingType>();
		this.targetMappedFrames = new HashMap<Frame, MappingType>();
		this.interestingFrames = new HashMap<Frame, Boolean>();
		this.mappingTypeManager = new MappingTypeManager();
		this.operationFilter = new CandidationOperationFilter();
		this.sourceFrameToMapElements = new HashMap<Frame, List<AbstractMapElement>>();
		this.targetFrameToMapElements = new HashMap<Frame, List<AbstractMapElement>>();
		this.frameToNumOfMappings = new HashMap<Frame, Integer>();
		largestNumOfMappings = 0;
	}

	public void reset() {
		sourcePaths.clear();
		targetPaths.clear();
		mappedElements.clear();
		sourceMappedFrames.clear();
		targetMappedFrames.clear();
		interestingFrames.clear();
		sourceFrameToMapElements.clear();
		targetFrameToMapElements.clear();
		frameToNumOfMappings.clear();
		largestNumOfMappings = 0;
	}

	public int getLargestNumberOfMappings() {
		return largestNumOfMappings;
	}

	public int getSize() {
		return mappedElements.size();
	}

	public MappingTypeManager getMappingTypeManager() {
		return mappingTypeManager;
	}

	public ArrayList<AbstractMapElement> getMappedElements() {
		return mappedElements;
	}

	public ArrayList<AbstractMapElement> getMappedElements(MappingType mappingType) {
		ArrayList<AbstractMapElement> mapElements = new ArrayList<AbstractMapElement>();
		for (AbstractMapElement element : mappedElements) {
			if (element.getMappingType().equals(mappingType)) {
				mapElements.add(element);
			}
		}

		return mapElements;
	}

	public int getNumOfMappings(Frame frame) {
		return frameToNumOfMappings.get(frame);
	}

	public HashMap<Frame, Boolean> getInterestingFrames() {
		return interestingFrames;
	}

	public Map<Frame, MappingType> getSourceFrames() {
		return sourceMappedFrames;
	}

	public Map<Frame, MappingType> getTargetFrames() {
		return targetMappedFrames;
	}

	public Map<Frame, List<AbstractMapElement>> getSourceFrameMappings() {
		return sourceFrameToMapElements;
	}

	public TreeSet<Collection<Frame>> getSourcePaths() {
		return sourcePaths;
	}

	public TreeSet<Collection<Frame>> getTargetPaths() {
		return targetPaths;
	}

	public AbstractMapElement getMapElement(Frame sourceFrame, Frame targetFrame) {
		List<AbstractMapElement> mappedElements = getMappedElements(sourceFrame);
		if (mappedElements != null) {
			for (AbstractMapElement element : mappedElements) {
				if (!isFiltered(element)) {
					if (element.getTargetFrame().equals(targetFrame)) {
						return element;
					}
				}
			}
		}

		return null;
	}

	/**
	 * Sets the operation filter type. This feature allows mappings to be filtered based on
	 * explanation types.
	 * 
	 * @param filterType
	 */
	public void setOperationFilter(int filterType) {
		operationFilter.setCurrentFilter(filterType);
	}

	public int getOperationFilter() {
		return operationFilter.getCurrentFilter();
	}

	/**
	 * Checks to see if the mapElement's mapping type is currently filtered.
	 */
	public boolean isFiltered(AbstractMapElement mapElement) {
		return isFiltered(mapElement.getMappingType()) || operationFilter.isFiltered(mapElement);
	}

	public void performExactNameMatches() {
		List<AbstractMapElement> tempMappedElements = new ArrayList<AbstractMapElement>();
		tempMappedElements.addAll(mappedElements);

		for (AbstractMapElement element : tempMappedElements) {
			Operation operation = null;
			//if(element.getSourceFrame().getBrowserText().equals(element.getTargetFrame().getBrowserText())) {
			if (element instanceof TemporaryMapElement) {
				operation = DataLoaderFacade.getInstance().getTemporaryMappingManager().getOperationForMapping(element);
			} else if (element instanceof CandidateMapElement) {
				operation = DataLoaderFacade.getInstance().getSuggestionsManager().getOperationForMapping(element);
			}

			if (operation != null && operation.getReason() != null) {
				boolean doOperation = false;
				for (Iterator iter = operation.getReason().iterator(); iter.hasNext();) {
					Explanation explanation = (Explanation) iter.next();
					if (explanation instanceof IdenticalNames) {
						doOperation = true;
						break;
					}
				}
				if (doOperation) {
					operation.performOperation();
				}
			}
			//}
		}
	}

	/**
	 * Checks whether the given frame has all of its map elements filtered.
	 * 
	 * @return True if all mapping elements from the frame have been filtered.
	 */
	public boolean isFiltered(Frame frame) {
		List<AbstractMapElement> mapElements = getMappedElements(frame);
		for (AbstractMapElement mapElement : mapElements) {
			if (!operationFilter.isFiltered(mapElement)) {
				return false;
			}
		}

		return true;
	}

	/**
	 * A mapping item is filtered if a search filter has been applied and the mapElement does not
	 * match that filter or if the control panel dialog has been used to filter the given
	 * mapElement's mapping type.
	 */
	public boolean isFiltered(MappingType mappingType) {
		MappingType sourceMappingType = TreeFilterManager.getInstance().getActiveMappingTypeFilter(TreeFilterManager.SOURCE_SEARCH);
		MappingType targetMappingType = TreeFilterManager.getInstance().getActiveMappingTypeFilter(TreeFilterManager.TARGET_SEARCH);

		if ((sourceMappingType != null && mappingType == sourceMappingType) || (targetMappingType != null && mappingType == targetMappingType)) {
			return false;
		} else if ((sourceMappingType != null && mappingType != sourceMappingType) || (targetMappingType != null && mappingType != targetMappingType)) {
			return true;
		}

		return !mappingTypeManager.isVisible(mappingType);
	}

	public boolean isLastPath(Collection<Frame> path, TreeSet<Collection<Frame>> paths) {
		if (path == null && paths.size() > 0) {
			return false;
		} else if (paths.size() == 0) {
			return true;
		}

		return paths.last().equals(path);
	}

	public boolean isFirstPath(Collection<Frame> path, TreeSet<Collection<Frame>> paths) {
		if (path == null || paths.size() == 0) {
			return true;
		}
		return paths.first().equals(path);
	}

	/**
	 * Finds the next path in the list of paths that comes after the currentPath and is not filtered
	 * in the view.
	 */
	public Collection<Frame> getNextPath(Collection<Frame> currentPath, TreeSet<Collection<Frame>> paths) {
		if (paths == null) {
			return null;
		}

		if (paths != null && paths.size() > 0) {
			TreeSet<Collection<Frame>> forwardPaths = paths;
			if (currentPath != null) {
				forwardPaths = (TreeSet<Collection<Frame>>) paths.tailSet(currentPath);
			}

			Object[] forwardsPathsArray = forwardPaths.toArray();
			for (int i = 1; i < forwardsPathsArray.length; i++) {
				if (forwardsPathsArray[i] instanceof Collection) {
					List<Frame> frames = (List<Frame>) forwardsPathsArray[i];
					Frame frame = frames.get(frames.size() - 1);
					MappingType mappingType = sourceMappedFrames.get(frame);
					if (mappingType == null) {
						mappingType = targetMappedFrames.get(frame);
					}

					if (!isFiltered(mappingType)) {
						return frames;
					}
				}
			}
		}

		return null;
	}

	/**
	 * Finds the previous path in the list of paths that comes after the currentPath and is not
	 * filtered in the view.
	 */
	public Collection<Frame> getPreviousPath(Collection<Frame> currentPath, TreeSet<Collection<Frame>> paths) {
		if (currentPath == null) {
			currentPath = paths.first();
		}

		if (paths != null && paths.size() > 0) {
			TreeSet<Collection<Frame>> previousPaths = (TreeSet<Collection<Frame>>) paths.headSet(currentPath);
			Object[] previousPathsArray = previousPaths.toArray();
			for (int i = previousPathsArray.length - 1; i >= 0; i--) {
				if (previousPathsArray[i] instanceof Collection) {
					List<Frame> frames = (List<Frame>) previousPathsArray[i];
					Frame frame = frames.get(frames.size() - 1);
					MappingType mappingType = sourceMappedFrames.get(frame);
					if (mappingType == null) {
						mappingType = targetMappedFrames.get(frame);
					}

					if (!isFiltered(mappingType)) {
						return frames;
					}
				}
			}
		}

		return null;
	}

	public boolean isSourceFrameMapped(Object frameValue) {
		return sourceMappedFrames.containsKey(frameValue);
	}

	public boolean isTargetFrameMapped(Object frameValue) {
		return targetMappedFrames.containsKey(frameValue);
	}
	
	public boolean isSourceFrameMapped(Object frameValue, MappingType mappingType) {
		return sourceMappedFrames.get(frameValue) == mappingType;
	}

	public boolean isTargetFrameMapped(Object frameValue, MappingType mappingType) {
		return targetMappedFrames.get(frameValue) == mappingType;
	}

	/**
	 * Moves the given MapElement to the back of the collection, which will draw it on top of the
	 * rest of the MapElements.
	 */
	public void moveToFront(AbstractMapElement mapElement) {
		mappedElements.remove(mapElement);
		mappedElements.add(mapElement);
	}

	public List<AbstractMapElement> getMappedElements(Frame frame) {
		List<AbstractMapElement> mapElements = sourceFrameToMapElements.get(frame);
		if (mapElements == null) {
			mapElements = targetFrameToMapElements.get(frame);
		}

		return mapElements;
	}

	/**
	 * Get a mapped element based on the tree path.
	 * 
	 * @return The found MapElement.
	 */
	@SuppressWarnings("unchecked")
	public ArrayList<AbstractMapElement> getMappedElements(TreePath path, boolean isSource) {
		ArrayList<AbstractMapElement> mapElements = new ArrayList<AbstractMapElement>();
		if (path != null) {
			for (AbstractMapElement element : mappedElements) {
				if (!isFiltered(element)) {
					LazyTreeNode node = (LazyTreeNode) path.getLastPathComponent();
					Frame frameForNode = FrameUtil.getFrameFromObject(node.getUserObject());

					if (isSource && frameForNode.equals(element.getSourceFrame())) {
						mapElements.add(element);
					} else if (!isSource && frameForNode.equals(element.getTargetFrame())) {
						mapElements.add(element);
					}
				}
			}
		}
		return mapElements;
	}

	/**
	 * Get a mapped element based on the tree path and restrict the results to the focalNode. For
	 * example, if path is a TreePath from the source ontology, than the focalFrame would be a Frame
	 * from the target ontology. We only want to return all mapping elements that go from the source
	 * TreePath to the focalFrame.
	 * 
	 * @return The found list of MapElements.
	 */
	public ArrayList<AbstractMapElement> getMappedElements(TreePath path, Frame focalFrame, boolean isSource) {
		ArrayList<AbstractMapElement> mapElements = new ArrayList<AbstractMapElement>();
		if (path != null) {
			for (AbstractMapElement element : mappedElements) {
				if (!isFiltered(element)) {
					LazyTreeNode node = (LazyTreeNode) path.getLastPathComponent();
					Frame frameForNode = FrameUtil.getFrameFromObject(node.getUserObject());

					if (isSource && frameForNode.equals(element.getSourceFrame()) && focalFrame.equals(element.getTargetFrame())) {
						mapElements.add(element);
					} else if (!isSource && frameForNode.equals(element.getTargetFrame()) && focalFrame.equals(element.getSourceFrame())) {
						mapElements.add(element);
					}
				}
			}
		}
		return mapElements;
	}

	/**
	 * Add a new mapping based on the source and target paths.
	 * 
	 * @return The newly created MapElement.
	 */
	public AbstractMapElement addMapping(AbstractMapElement mapElement) {
		if (mappedElements.contains(mapElement)) {
			System.out.println("Already contains that mapping");
		} else {
			mappedElements.add(mapElement);
			
			// save the frames - these are used by the frame renderer to add the mapped icon
			saveMappingType(sourceMappedFrames, mapElement.getSourceFrame(), mapElement.getMappingType());
			saveMappingType(targetMappedFrames, mapElement.getTargetFrame(), mapElement.getMappingType());

			addMapping(mapElement, mapElement.getSourceFrame(), sourceFrameToMapElements);
			addMapping(mapElement, mapElement.getTargetFrame(), targetFrameToMapElements);

			sourcePaths.add(mapElement.getSourcePath());
			targetPaths.add(mapElement.getTargetPath());
		}

		return mapElement;
	}
	
	private void saveMappingType(Map<Frame, MappingType> frameToMappingTypeMap, Frame frame, MappingType mappingType) {
		MappingType currentMappingType = frameToMappingTypeMap.get(frame);
		if(currentMappingType == null || !currentMappingType.equals(MappingType.VERIFIED_MAPPING)) {
			frameToMappingTypeMap.put(frame, mappingType);
		}
	}

	/**
	 * Removes all mappings matching
	 * 
	 * @param mapElement
	 */
	public void removeMappings(AbstractMapElement mapElement) {
		sourceMappedFrames.remove(mapElement.getSourceFrame());
		targetMappedFrames.remove(mapElement.getTargetFrame());
		sourceFrameToMapElements.remove(mapElement.getSourceFrame());
		targetFrameToMapElements.remove(mapElement.getTargetFrame());
		mappedElements.remove(mapElement);
		if (mapElement instanceof TemporaryMapElement) {
			DataLoaderFacade.getInstance().getTemporaryMappingManager().removeTemporaryMapping(mapElement);
		} else if (mapElement instanceof CandidateMapElement) {
			DataLoaderFacade.getInstance().getSuggestionsManager().removeCandidateMapping(mapElement);
		} else {
			Cls sourceCls = (Cls) mapElement.getSourceFrame();
			Cls targetCls = (Cls) mapElement.getTargetFrame();

			MappingStoragePlugin mappingStoragePlugin = PluginManager.getInstance().getFirstActiveMappingStoragePlugin();
			mappingStoragePlugin.removeClassToClassMapping(sourceCls, targetCls);

			// update Prompt
			Operation suggestion = MergeFramesOperation.selectMergeOperation(sourceCls, targetCls, new RejectedMapping(sourceCls, targetCls));
			suggestion.setPriority(0);

			SuggestionsAndConflicts.getSuggestionsList().removeOldAction(suggestion);
			SuggestionsAndConflicts.addSuggestions(CollectionUtilities.createCollection(suggestion), true);

			//TabComponent.updateUI(null);
			SuggestionsAndConflicts.executePendingMoves();

			// refresh our list of mappings
			//OntologyFacade.getInstance().refreshMapping();
		}
	}

	/**
	 * Adds a mapping to the internal Frame to map element map. This map is used for fast look up of
	 * what map elements a frame has.
	 */
	private void addMapping(AbstractMapElement mapElement, Frame frame, Map<Frame, List<AbstractMapElement>> frameToMapElements) {
		List<AbstractMapElement> mappings = frameToMapElements.get(frame);
		if (mappings == null) {
			mappings = new ArrayList<AbstractMapElement>();
		}
		mappings.add(mapElement);
		frameToMapElements.put(frame, mappings);

		int numOfMappings = getNumOfMappings(mappings);
		frameToNumOfMappings.put(frame, numOfMappings);

		largestNumOfMappings = Math.max(largestNumOfMappings, numOfMappings);
	}

	private int getNumOfMappings(List<AbstractMapElement> mapElements) {
		int numOfMappings = 0;
		if (mapElements != null && mapElements.size() > 0) {
			Collection<Frame> sourcePath = mapElements.get(0).getSourcePath();
			Set<Frame> uniqueTargetFrames = new HashSet<Frame>();

			for (AbstractMapElement mapElement : mapElements) {
				if (mapElement.getSourcePath().equals(sourcePath)) {
					if (!uniqueTargetFrames.contains(mapElement.getTargetFrame())) {
						numOfMappings++;
						uniqueTargetFrames.add(mapElement.getTargetFrame());
					}
				}
			}
		}

		return numOfMappings;
	}
}

class FramePathComparator implements Comparator<Collection<Frame>> {
	public int compare(Collection<Frame> o1, Collection<Frame> o2) {
		if (o2 == null) {
			return 1;
		}

		Iterator<Frame> iter1 = o1.iterator();
		Iterator<Frame> iter2 = o2.iterator();
		Frame parentFrame = null;
		while (iter1.hasNext() && iter2.hasNext()) {
			Frame f1 = iter1.next();
			Frame f2 = iter2.next();

			if (f1.getBrowserText().equalsIgnoreCase(f2.getBrowserText())) {
				parentFrame = f1;
			} else {
				return FrameUtil.compareOrder(f1, f2, parentFrame);
			}
		}

		// paths matched or partially matched
		if (o1.size() > o2.size()) {
			return 1;
		} else if (o2.size() > o1.size()) {
			return -1;
		}
		return 0;
	}
}
